11.3 智能指针¶
C++内存¶
我们的C++程序主要包含如下三种内存:
静态内存:保存局部
static对象、类static数据成员以及定义在任何函数之外的变量栈内存:保存定义在函数内的非
static对象自由空间/堆:存储动态分配的对象,即在程序运行时分配的对象
从动态内存到智能指针¶
1. 动态内存管理¶
在C++中,动态内存的管理是通过一对运算符来完成的:
new:在动态内存中为对象分配空间并返回一个指向该对象的指针delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存
3. 智能指针¶
Tips:下述三种智能指针都定义在
memory头文件中。
为了更容易且更安全地使用动态内存,新的标准库提供了智能指针类型来管理动态对象:
shared_ptr:允许多个指针指向同个对象unique_ptr:“独占”所指向的对象weak_ptr:弱引用,指向shared_ptr所管理的对象
C++类使用动态生存期资源的原因¶
在C++类外使用动态生存期资源可能是为了手动管理对象的生命周期或者是因为栈空间不足,而在C++类中使用动态生存期资源主要出于如下三种原因:
程序不知道自己需要多少对象
程序不知道所需对象的准确类型
程序需要在多个对象间共享数据
1. 程序不知道自己需要多少对象¶
某些类需要在运行时分配可变大小的内存空间,这些类通常可以(如果确实可以的话,一般应该)使用标准库容器来保存它们的数据。容器类就是出于第一种原因而使用动态内存的典型例子。
2. 程序不知道所需对象的准确类型¶
当我们希望在容器中存放具有继承关系的对象时,我们实际上存放的通常是基类的指针(更好的选择是智能指针),这些指针所指对象的动态类型可能是基类类型,也可能是派生类类型:
// 我们可以将一个派生类的(智能)指针转换为基类的(智能)指针, make_shared<Bulk_quote>返回shared_ptr<Bulk_quote>, 但是调用push_back时该对象被转换为shared_ptr<Quote>
vector<shared_ptr<Quote>> basket;
basket.push_back(make_shared<Quote>("0-201-82470-1", 50));
basket.push_back(make_shared<Bulk_quote>("0-201-54848-8", 50, 10, .25));、
// 调用Quote定义的版本; 打印562.5, 即在15*50中扣掉折扣金额
cout << basket.back()->net_price(15) << endl;
因为basket存放着shared_ptr,所以我们必须解引用basket.back()的返回值以获得运行net_price的对象,实际上调用的net_price版本取决于指针所指对象的动态类型。
3. 程序需要在多个对象间共享数据¶
到目前为止,我们使用的类分配的资源都与对应对象生存期一致。例如每个vector“拥有”自己的元素,当我们拷贝一个“vector”时,原vector和副本vector中的元素是相互分离的。但是某些类分配的资源具有与原对象相独立的生存期。
例如假定我们希望定义一个名为StrBlob的类,保存一组string元素。与容器不同,我们希望Blob对象的不同拷贝之间共享相同的元素。即当我们拷贝一个StrBlob时,原StrBlob对象及其拷贝应该引用相同的底层元素。
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <stdexcept>
using std::vector;
// StrBlob: 管理string的类, StrBlob对象的不同拷贝之间共享相同的底层数据
class StrBlob {
public:
typedef std::vector<std::string>::size_type size_type;
// 默认构造函数
StrBlob() : data(std::make_shared<std::vector<std::string>>()) { }
// 接收初始化列表的构造函数
StrBlob(std::initializer_list<std::string> il) :
data(std::make_shared<std::vector<std::string>>(il)) { }
// 返回data的大小
size_type size() const { return data->size(); }
// 返回data是否为空
bool empty() const { return data->empty(); }
// 添加元素
void push_back(const std::string &t) { data->push_back(t); }
// 删除元素
void pop_back();
// 元素访问
std::string& front();
std::string& back();
private:
// StrBlob对象的底层数据
std::shared_ptr<std::vector<std::string>> data;
// 如果data[i]不合法, 则抛出异常
void check(size_type i, const std::string &msg) const {
if (i >= data->size())
throw std::out_of_range(msg);
}
};
std::string& StrBlob::front() {
check(0, "front on empty StrBlob");
return data->front();
}
std::string& StrBlob::back() {
check(0, "back on empty StrBlob");
return data->back();
}
void StrBlob::pop_back() {
check(0, "pop_back on empty StrBlob");
data->pop_back();
}
int main(void) {
// 空StrBlob
StrBlob b1;
// 此时b1管理的底层data为空
std::cout << "b1.size(): " << b1.size() << std::endl;
{ // 新作用域
StrBlob b2 = {"tomo", "cat", "tomocat"};
b1 = b2;
} // 离开作用域, b2被销毁, 但b1指向最初由b2创建的元素
std::cout << "b1.size(): " << b1.size() << std::endl;
}
// 输出:
b1.size(): 0
b1.size(): 3
C++类管理动态生存期资源的表现:行为像值或者指针¶
通常管理类外资源的类需要通过析构函数来释放对象所分配的资源,根据“三/五原则”它也必须自定义拷贝构造函数和拷贝赋值运算符(delete拷贝构造函数和拷贝赋值运算符也算自定义的一种)。
对于管理类外资源的类,根据如何拷贝指针成员我们可以大致分为如下三类:
既不像值也不像指针的类:
IO类型和unique_ptr这种不允许拷贝和赋值的类行为像值的类:标准库容器和
string类行为像指针的类:
shared_ptr
1. 行为像值的类¶
为了提供类值的行为,对于类管理的资源,每个对象都应该有自己的一份拷贝。以管理string资源的类HasPtr的类而言:
拷贝构造函数:完成
string的拷贝而不是拷贝指针析构函数:释放
string对象拷贝赋值运算符:释放对象当前的
string,并从右侧运算对象拷贝string
class HasPtr {
public:
// 构造函数: 分配string动态内存
explicit HasPtr(const std::string &s) : ps_(new std::string(s)) { }
// 拷贝构造函数
HasPtr(const HasPtr &p) : ps_(new std::string(*p.ps_)) { }
// 拷贝赋值运算符
HasPtr& operator=(const HasPtr &);
// 析构函数: 释放构造函数中分配的动态内存
~HasPtr() { delete ps_; }
// 类自定义的swap成员函数
friend void swap(HasPtr&, HasPtr&);
private:
std::string *ps_;
};
// 拷贝赋值运算符:
// 1) 组合了析构函数和拷贝构造函数: 先销毁左侧运算对象资源, 然后从右侧运算对象拷贝数据
// 2) 自赋值安全: 如果将一个对象赋予它自身, 赋值运算符必须能正确工作
// 3) 异常安全: 当异常发生时能将左侧运算对象置于一个有意义的状态
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
auto newp = new std::string(*rhs.ps_); // 拷贝底层string
delete ps_; // 释放本对象的旧内存
ps_ = newp; // 从右侧运算对象拷贝数据到本对象
return *this;
}
2. 行为像指针的类¶
令一个类展现类似指针的行为的最好方法是使用shared_ptr来管理类中的资源,拷贝(或赋值)一个shared_ptr会拷贝(或赋值)shared_ptr所指向的指针。shared_ptr类会自己记录有多少用户共享它所指向的对象,当没有用户使用对象时,shared_ptr类负责释放资源。
3. swap交换操作¶
Tips:管理动态资源的类通常除了自定义拷贝控制成员外,还需要定义一个名为
swap的函数。如果一个类定义了自己的swap成员函数,那么算法将使用类自定义版本,否则算法将使用标准库定义的swap。
// 交换指针而非string数据, 提高性能
inline void swap(HasPtr &lhs, HasPtr &rhs) {
std::swap(lhs.ps_, rhs.ps_);
}
定义了swap的类通常用swap来定义它们的“拷贝并交换赋值运算符”,这些运算符使用了一种名为拷贝并交换copy and swap的技术,将左侧运算对象与右侧运算对象的一个副本进行交换:
Tips:
这种技术天生是自赋值安全且异常安全的,一方面它通过在改变左侧运算对象之前拷贝右侧运算对象保证了自赋值的安全性,另一方面代码唯一可能抛出异常的是拷贝构造函数中的
new表达式,如果真的抛出异常也是在我们改变左侧运算对象之前发生由于接受的参数并不是一个引用,因此该参数需要进行拷贝初始化,既有可能调用拷贝构造函数(左值)也有可能调用移动构造函数(右值)
当类定义了移动构造函数时,拷贝并交换赋值运算符也会为该类实现一个移动赋值运算符
// 拷贝并交换赋值运算符既是移动赋值运算符也是拷贝赋值运算符:
// 1) 参数并不是一个引用: 调用拷贝/移动构造函数以值传递传入一个右侧运算对象的副本
// 2) 交换左侧运算对象与右侧运算对象的副本
HasPtr& HasPtr::operator=(HasPtr rhs) {
swap(*this, rhs); // rhs现在指向本对象曾经使用过的内存
return *this; // rhs销毁, 从而delete了rhs中的指针
}
智能指针与异常¶
如果使用智能指针,那么即使程序块过早结束,那么智能指针类也能确保在内存不再需要时将其释放:
void f() {
shared_prt<int> sp(new int(42)); // 分配一个新对象
// 这段代码抛出异常, 且在f中未被捕获
} // 函数结束时shared_ptr自动释放内存
与之相对的是,如果使用内置指针管理内存 ,且在new之后在对应的delete之前发生了异常,则内存不会被释放:
void f() {
int *pi = new int(42); // 动态分配一个新对象
// 这段代码抛出异常, 且在f中未被捕获
delete pi; // 抛出异常后不会走到这里, 这块内存不会被释放
}
unique_ptr¶
编码规范:如果必须使用动态内存分配,那么更倾向于将所有权保存在分配者手中,如果其他地方要使用这个对象,最好用
std::unique_ptr来明确所有权传递,例如:// 分配动态内存 std::unique_ptr<Foo> FooFactory(); // 使用动态内存 void FooConsumer(std::unique_ptr<Foo> ptr);
1. 简介¶
一个unique_ptr“拥有”它所指向的对象,与shared_ptr不同,某个时刻只能有一个unique_ptr指向一个给定对象。当unique_ptr被销毁时,它所指向的对象也被销毁。
2. 提供的操作¶
| 操作 | 含义 | 备注 |
|---|---|---|
unique_ptr<T> u1 |
空unique_ptr对象,u1调用delete来释放它的指针 |
|
unique_ptr<T, D> u2 |
空unique_ptr对象,u2会使用一个类型为D的可调用对象来释放它的指针 |
|
unique_ptr<T, D> u(d) |
空unique_ptr对象,指向类型为T的对象,用类型为D的对象d来代替delete |
|
u = nullptr |
释放u指向的对象,将u置为空 |
|
u.release() |
u放弃对指针的控制权,返回指针,并将u置为空 |
|
u.reset()u.reset(q)u.reset(nullptr) |
释放u指向的对象,如果提供了内置指针q,令u指向这个对象;否则将u置为空 |
3. 转移unique_ptr指针的所有权¶
Tips:
reset()方法接受一个可选的指针参数,令unique_ptr重新指向给定的指针,如果unique_ptr不为空,则它原来指向的对象将被释放。调用release()方法会切断unique_ptr和它原来管理的对象之间的联系,release()方法返回的指针通常被用来初始化另一个智能指针或者给另一个智能指针赋值。如果我们不用另一个智能指针来保存release()返回的指针,那么我们的程序就必须负责资源的释放。
虽然我们不能拷贝或赋值unique_ptr,但是我们可以通过release或reset将指针的所有权从一个非const的unique_ptr转移到另一个unique_ptr:
// release()将指针所有权从p1转向p2, 并将p1置为空
unique_ptr<string> p1(new string("tomocat"));
unique_ptr<string> p2(p1.release());
// 将所有权从p3转向p2
unique_ptr<string> p3(new string("cat"));
p2.reset(p3.release());
不能拷贝unique_ptr有一个例外:我们可以拷贝或赋值将要被销毁的unique_ptr,最常见的例子就是从函数返回一个unique_ptr:
unique_ptr<int> clone(int p) {
// 正确: 从int*创建一个unique_ptr<int>
return unique_ptr<int>(new int(p));
}
// 还可以返回一个局部对象的拷贝
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int(p));
// ...
return ret;
}
weak_ptr¶
1. 简介¶
Tips:一旦最后一个指向对象的
shared_ptr被销毁,即使有weak_ptr指向对象,该对象也还是会被释放。
weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个shared_ptr管理的对象,但是将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。
2. 提供的操作¶
| 操作 | 含义 | 备注 |
|---|---|---|
weak_ptr<T> w |
空weak_ptr,可以指向类型为T的对象 |
|
weak_ptr<T> w(sp) |
与shared_ptr sp指向相同对象的weak_ptr |
|
w = p |
p可以是一个shared_ptr或是一个weak_ptr,赋值后w和p共享对象 |
|
w.reset() |
将w置为空 |
|
w.use_count() |
与w共享对象的shared_ptr数量 |
|
w.expired() |
若w.use_count()为0则返回true |
|
w.lock() |
如果expired为true,返回一个空shared_ptr,否则返回一个指向w的shared_ptr |
3. 访问对象¶
由于对象可能不存在,因此我们不能直接使用weak_ptr访问对象,必须调用lock函数检查指向的对象是否仍存在:
// 初始化一个weak_ptr
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);
// 判断weak_ptr指向的对象是否仍存在
if (shared_ptr<int> np = wp.lock()) {
// do something
}
智能指针和哑类¶
包括所有标准库在内的很多C++类都定义了析构函数,负责清理对象使用的资源。但是不是所有的类都是这么良好定义的,特别是哪些为C和C++两种语言设计的类,通常都要求用户显式地释放所使用的任何资源。
Tips:那些分配了资源,但是又没有定义析构函数来释放这些资源的类,可能会遇到与使用动态内存相同的错误——程序员非常容易忘记释放内存。另外,如果在资源分配和释放之间发生了异常,程序也会发生资源泄漏。
假定我们在使用一个C和C++都使用的网络库,其中connection没有析构函数来释放它的资源,我们可以使用shared_ptr来保证connection被正确关闭:
/*
* 未使用shared_ptr时
*/
struct destination; // 表示我们正在连接什么
struct connection; // 使用连接所需的信息
connection connect(destination*); // 打开连接
void disconnect(connection); // 关闭给定的连接
void f(destination &d /* 其他参数 */) {
// 获得一个连接, 程序员需要保证在使用后关闭它
connection c = connect(&d);
// 使用连接
// 如果我们在f退出前忘记调用disconnect, 就无法关闭c了
}
/*
* 使用shared_ptr
*/
// 自定义一个删除器
void end_connection(connection *p) { disconnect(*p); }
void f(destination &d /* 其他参数 */ ) {
connection c = connect(&d);
shared_ptr<connection> p(&c, end_connection);
// 使用连接
// 当f退出时(即使是因为异常而退出), connection也会被正确关闭
}
智能指针陷阱¶
智能指针可以提供对动态分配的内存安全而又方便的管理,但这建立在正确使用的前提下。为了正确使用智能指针,我们必须坚持一些基本规范:
不使用相同的内置指针值初始化(或
reset)多个智能指针不
delete get()返回的指针不使用
get()初始化或reset另一个智能指针如果你使用
get()返回的指针,记得当最后一个对应的智能指针销毁后,你的指针就失效了如果你使用智能指针管理的资源不是
new分配的内存,记住传递给它一个删除器
智能指针与动态数组¶
标准库提供了一个可以管理new分配数组的unique_ptr版本,对应的操作如下:
Tips:指向动态数组的
unique_ptr不支持成员访问运算符(点和箭头运算符),其他unique_ptr操作不变。
| 操作 | 含义 |
|---|---|
unique_ptr<T[]> u |
u可以指向一个动态分配的数组,数组元素类型为T |
unique_ptr<T[]> u(p) |
u可以指向内置指针p所指向的动态分配的数组 |
u[i] |
返回u拥有的数组中位置i处的对象 |
// up指向10个未初始化的int数组, 当up销毁它管理的指针时会自动使用delete[]
unique_ptr<int[]> up(new int[10]);
// 使用下标运算符来访问数组元素
for (size_t i = 0; i != 10; ++i) {
up[i] = i;
}
shared_ptr不支持管理动态数组,强行管理的话必须提供自己定义的删除器:
// 定义一个shared_ptr管理int数组, 传递一个lambda表达式作为删除器
shared_ptr<int> sp(new int[10], [](int *p) { delete[] p; });
// shared_ptr未定义下标运算符, 并且不支持指针的算数运算
for (size_t i = 0; i != 10; ++i) {
*(sp.get() + i) = i;
}
// 使用lambda释放数组
sp.reset();
编码规范:以独立语句将newed对象置入智能指针¶
Effective C++:Store newed objects in smart pointers in standalone statements.
以独立语句将newed对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露。
假设我们有一个函数用来揭示处理程序的优先级,另一个函数用来在某动态分配所得的Widget上进行某些带有优先权的处理:
int priority(); // 可能抛出异常
void processWidget(std::shared_ptr<Widget>pw, int priority);
调用过程如下:
processWidget(std::shared_ptr<Widget>(new Widget), priority());
在调用processWidget()之前,编译器必须创建代码做如下三件事情:
调用
priority()执行
new Widget调用
std::shared_ptr<Widget>构造函数
C++编译器以什么样的次序完成这些事情呢,这和其他语言如Java和C#不同,那两种语言总是以特定次序完成函数参数的核算。可以确定的是new Widget一定执行于std::shared_ptr构造函数被调用之前,因为这个表达式结果还要被传递作为std::shared_ptr<Widget>构造函数的一个实参,但对priority的调用则可以排在第一或第二或第三执行。如果编译器选择以第二顺序执行它(说不定可因此生成更高效的代码),最终获得这样的操作序列:
执行
new Widget调用
priority()调用
std::shared_ptr<Widget>构造函数
万一对priority的调用导致异常,此时new Widget返回的指针会遗失,因为它尚未被置入std::shared_ptr<Widget>内,后者是我们期盼用来防卫资源泄露的武器。在对processWidget的调用过程中可能发生资源泄露,因为在“资源被创建”和“资源被转换为资源管理对象”两个时间点之间可能发生异常干扰。
避免这类问题办法就是使用分离语句:
// 在单独语句内以智能指针存储newed所得对象
std::shared_ptr<Widget> pw(new Widget);
// 这个调用动作绝不至于造成泄露
processWidget(pw, priority());
Reference¶
[1] https://www.cnblogs.com/tenosdoit/p/3456704.html
[2] https://zhuanlan.zhihu.com/p/54078587